package com.solo.codegen.web;

import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.mybatisflex.core.paginate.Page;
import com.solo.codegen.api.entity.GenModule;
import com.solo.codegen.model.code.resp.CodePreviewResp;
import com.solo.codegen.model.module.GenModuleConvert;
import com.solo.codegen.model.module.req.ModuleCreateReq;
import com.solo.codegen.model.module.req.ModuleQueryReq;
import com.solo.codegen.model.module.req.ModuleUpdateReq;
import com.solo.codegen.model.module.resp.ModuleGetResp;
import com.solo.codegen.model.module.resp.ModuleListResp;
import com.solo.codegen.service.GenModuleService;
import com.solo.common.core.global.R;
import com.solo.common.logger.annotation.Logger;
import com.solo.common.logger.enums.LoggerType;
import com.solo.common.orm.core.query.Wrappers;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.tika.Tika;
import org.springframework.web.bind.annotation.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 模块控制器
 * @author 十一
 * @since 2024-10-18 15:41
 * 人生若只如初见，何事秋风悲画扇
 **/
@RestController
@RequiredArgsConstructor
@RequestMapping("/codegen/module")
public class GenModuleController {

    private final GenModuleService genModuleService;

    /**
     * 新增模块
     * @param req 模块新增对象
     * @return 响应信息
     */
    @PostMapping
    @SaCheckPermission("codegen-module-create")
    @Logger(value = "新增模块", type = LoggerType.CREATE)
    public R<Boolean> create(@Valid @RequestBody ModuleCreateReq req) {
        GenModule entity = GenModuleConvert.INSTANCE.convert(req);
        return R.success(genModuleService.save(entity));
    }

    /**
     * 删除模块
     * @param moduleIds 模块集合
     * @return 响应信息
     */
    @DeleteMapping("/{moduleIds}")
    @SaCheckPermission("codegen-module-delete")
    @Logger(value = "删除模块", type = LoggerType.DELETE)
    public R<Boolean> delete(@PathVariable Long[] moduleIds) {
        return R.success(genModuleService.removeByIds(Arrays.asList(moduleIds)));
    }

    /**
     * 更新模块
     * @param req 模块更新对象
     * @return 响应信息
     */
    @PutMapping
    @SaCheckPermission("codegen-module-update")
    @Logger(value = "更新模块", type = LoggerType.UPDATE)
    public R<Boolean> update(@Valid @RequestBody ModuleUpdateReq req) {
        GenModule entity = GenModuleConvert.INSTANCE.convert(req);
        return R.success(genModuleService.updateById(entity));
    }

    /**
     * 获取模块
     * @param moduleId 模块
     * @return 响应信息
     */
    @GetMapping("/{moduleId}")
    @SaCheckPermission("codegen-module-query")
    public R<ModuleGetResp> get(@PathVariable Long moduleId) {
        return R.success(GenModuleConvert.INSTANCE.convertGet(genModuleService.getById(moduleId)));
    }

    /**
     * 分页查询模块列表
     * @param page 分页对象
     * @param req 模块查询对象
     * @return 响应信息
     */
    @GetMapping("/page")
    @SaCheckPermission("codegen-module-query")
    public R<Page<ModuleListResp>> page(Page<ModuleListResp> page, ModuleQueryReq req) {
        Page<ModuleListResp> list = genModuleService.pageAs(page, Wrappers.builder(req), ModuleListResp.class);
        return R.success(list);
    }

    /**
     * 代码预览
     * @param moduleId 业务表id
     * @return 响应信息
     */
    @GetMapping("/preview/{moduleId}")
    @SaCheckPermission("codegen-module-preview")
    public R<List<CodePreviewResp>> preview(@PathVariable Long moduleId) {
        Map<String, String> codes = genModuleService.generationCodes(moduleId);
        List<CodePreviewResp> collect = codes.entrySet().stream().map(entry -> {
            CodePreviewResp respVO = new CodePreviewResp();
            respVO.setPath(entry.getKey());
            respVO.setCode(entry.getValue());
            return respVO;
        }).collect(Collectors.toList());
        return R.success(collect);
    }

    /**
     * 代码生成
     * @param moduleId 模块表id
     * @param response 响应对象
     * @throws IOException io异常
     */
    @GetMapping("/generate-code/{moduleId}")
    @SaCheckPermission("codegen-module-generate-code")
    public void gen(@PathVariable Long moduleId, HttpServletResponse response) throws IOException {
        // 生成代码
        Map<String, String> codes = genModuleService.generationCodes(moduleId);
        // 构建 zip 包
        String[] paths = codes.keySet().toArray(new String[0]);
        ByteArrayInputStream[] ins = codes.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipUtil.zip(outputStream, paths, ins);
        // 输出
        writeAttachment(response, "module.zip", outputStream.toByteArray());
    }

    private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new);

    /**
     * 获得文件的 mineType，对于doc，jar等文件会有误差
     *
     * @param data 文件内容
     * @return mineType 无法识别时会返回“application/octet-stream”
     */
    @SneakyThrows
    public static String getMineType(byte[] data) {
        return TIKA.get().detect(data);
    }

    /**
     * 已知文件名，获取文件类型，在某些情况下比通过字节数组准确，例如使用jar文件时，通过名字更为准确
     *
     * @param name 文件名
     * @return mineType 无法识别时会返回“application/octet-stream”
     */
    public static String getMineType(String name) {
        return TIKA.get().detect(name);
    }

    /**
     * 在拥有文件和数据的情况下，最好使用此方法，最为准确
     *
     * @param data 文件内容
     * @param name 文件名
     * @return mineType 无法识别时会返回“application/octet-stream”
     */
    public static String getMineType(byte[] data, String name) {
        return TIKA.get().detect(data, name);
    }

    /**
     * 返回附件
     *
     * @param response 响应
     * @param filename 文件名
     * @param content  附件内容
     */
    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
        // 设置 header 和 contentType
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
        String contentType = getMineType(content, filename);
        response.setContentType(contentType);
        // 针对 video 的特殊处理，解决视频地址在移动端播放的兼容性问题
        if (StrUtil.containsIgnoreCase(contentType, "video")) {
            response.setHeader("Content-Length", String.valueOf(content.length - 1));
            response.setHeader("Content-Range", String.valueOf(content.length - 1));
            response.setHeader("Accept-Ranges", "bytes");
        }
        // 输出附件
        IoUtil.write(response.getOutputStream(), false, content);
    }
}
